package com.small.nine.wxmp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.http.HttpUtil;
import com.small.nine.wxmp.common.constant.RedisConstant;
import com.small.nine.wxmp.common.constant.WxConstant;
import com.small.nine.wxmp.common.lock.RedisLock;
import com.small.nine.wxmp.config.WxConfig;
import com.small.nine.wxmp.domain.bean.wx.WxAccessTokenBean;
import com.small.nine.wxmp.domain.bean.wx.WxInMsgBean;
import com.small.nine.wxmp.domain.bean.wx.WxMenuBean;
import com.small.nine.wxmp.domain.bean.wx.WxMenuButtonBean;
import com.small.nine.wxmp.domain.bean.wx.kf.WxKfAccountBean;
import com.small.nine.wxmp.domain.bean.wx.qrCode.WxQrCodeBean;
import com.small.nine.wxmp.domain.bean.wx.qrCode.WxTicketBean;
import com.small.nine.wxmp.domain.bean.wx.template.*;
import com.small.nine.wxmp.service.WxAsyncService;
import com.small.nine.wxmp.service.WxService;
import com.small.nine.wxmp.utils.wx.JsonUtils;
import com.small.nine.wxmp.utils.wx.crypt.WxMsgCrypt;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author sheng_zs@126.com
 * @date 2021-07-26 9:24
 */
@Slf4j
@Service
public class WxServiceImpl implements WxService {
    @Resource
    private WxConfig wxConfig;
    @Resource
    private ValueOperations<String, Object> valueOperations;
    @Resource
    private RedisLock redisLock;
    @Resource
    private WxAsyncService wxAsyncService;

    @Override
    public String checkSignature(String signature, String timestamp, String nonce, String echoStr) {
        log.info("微信接入：signature={}, timestamp={}, nonce={}, echoStr={}", signature, timestamp, nonce, echoStr);
        return checkToken(signature, timestamp, nonce, wxConfig.getToken()) ? echoStr : "非法请求";
    }

    @Override
    public String msgHandle(WxInMsgBean msgBean, String signature,
                            String timestamp, String nonce, String encType, String msgSignature) {
        /*
            验证是否是正常请求
         */
        if (!checkToken(signature, timestamp, nonce, wxConfig.getToken())) {
            throw new IllegalArgumentException("微信-非法请求，可能属于伪造请求");
        }
        WxInMsgBean bean = msgBean;
        log.info("WxInMsgBean: {}", bean.toString());

        /*
            验证是否是加密的信息
         */
        boolean isCrypt;
        WxMsgCrypt crypt = null;
        if (isCrypt = (Objects.equals("aes", encType) && StrUtil.isNotBlank(msgSignature))) {
            crypt = new WxMsgCrypt(wxConfig);
            bean = crypt.decrypt(msgSignature, timestamp, nonce, bean.getEncrypt());
            log.info("解密后信息：{}", bean.toString());
        }

        String msg = WxConstant.MSG_SUCCESS;
        // 排重
        if (isRepeat(bean.getMsgId(), bean.getFromUserName(), bean.getCreateTime())) {
            return isCrypt ? crypt.encrypt(msg) : msg;
        }
//
//        switch (bean.getMsgType()) {
//            case WxConstant.MSG_TYPE_TEXT:
//                // 处理 文本信息
//                msg = WxTextMsgBean.getInstance(bean.getFromUserName(), bean.getToUserName(), bean.getCreateTime(), bean.getContent()).toXml();
//                break;
//            case WxConstant.MSG_TYPE_IMAGE:
//                // 处理图片信息
//                msg = WxImageMsgBean.getInstance(bean.getFromUserName(), bean.getToUserName(), bean.getCreateTime(), bean.getMediaId()).toXml();
//                break;
//            case WxConstant.MSG_TYPE_EVENT:
//                // 处理事件
//                switch (bean.getEvent()) {
//                    case WxConstant.EVENT_TYPE_SUBSCRIBE:
//                        // 用户关注公众号
//                        ArticlesBean articlesBean = ArticlesBean.getInstance("标题", "图文信息", "http://mmbiz.qpic.cn/mmbiz_jpg/8IpUia88gYWiaj07aFP5jcHH5gDlicXhlgNwS2gxfEbC1T4NLGAr9qowOPLHIXBHFhewUxaogqcDMHgcmnZyt5MibA/0", "http://www.baidu.com");
//                        ArticlesBean articlesBean1 = ArticlesBean.getInstance("标题", "图文信息", "http://mmbiz.qpic.cn/mmbiz_jpg/8IpUia88gYWiaj07aFP5jcHH5gDlicXhlgNwS2gxfEbC1T4NLGAr9qowOPLHIXBHFhewUxaogqcDMHgcmnZyt5MibA/0", "http://www.baidu.com");
//                        ArticlesBean articlesBean2 = ArticlesBean.getInstance("标题", "图文信息", "http://mmbiz.qpic.cn/mmbiz_jpg/8IpUia88gYWiaj07aFP5jcHH5gDlicXhlgNwS2gxfEbC1T4NLGAr9qowOPLHIXBHFhewUxaogqcDMHgcmnZyt5MibA/0", "http://www.baidu.com");
//
//                        msg = WxNewsMsgBean.getInstance(bean.getFromUserName(), bean.getToUserName(), bean.getCreateTime(), articlesBean, articlesBean1, articlesBean2).toXml();
//                        break;
//                    case WxConstant.EVENT_TYPE_UNSUBSCRIBE:
//                        // 用户取消关注
//                        // 加入黑名单、删除数据库等操作
//                        break;
//                    default:
//                        msg = WxTextMsgBean.getInstance(bean.getFromUserName(), bean.getToUserName(), bean.getCreateTime(), "事件推送").toXml();
//                        break;
//                }
//                break;
//            default:
//                msg = WxTextMsgBean.getInstance(bean.getFromUserName(), bean.getToUserName(), bean.getCreateTime(), "暂时只支持文本、图片消息").toXml();
//                break;
//        }

        // 异步处理
        wxAsyncService.kfSend(bean);
        log.info("异步处理 ++++++++++ start ++++++++++");

        log.info("回复信息 msg: {}", msg);
        return isCrypt ? crypt.encrypt(msg) : msg;
    }

    @Override
    public String getAccessToken() {
        final String key = RedisConstant.WX_ACCESS_TOKEN_KEY + wxConfig.getAppID();
        /*
            1. 先获取 redis 上的 token
         */
        Object o = valueOperations.get(key);
        String token = "";
        if (Objects.nonNull(o) && StrUtil.isNotBlank(token = o.toString())) {
            return token;
        }
        // 锁
        RLock lock = redisLock.getRLock(RedisConstant.PARAM_ACCESS_TOKEN);
        lock.lock();
        try {
            // 防止不是第一个拿到锁
            o = valueOperations.get(key);
            if (Objects.nonNull(o) && StrUtil.isNotBlank(token = o.toString())) {
                return token;
            }
            token = forceRefreshAccessToken();
        } catch (Exception e) {
            log.error("微信公众号 {} 获取 access_token 失败", wxConfig.getAppID());
        } finally {
            lock.unlock();
        }
        return token;
    }

    @Override
    public String forceRefreshAccessToken() {
        final String key = RedisConstant.WX_ACCESS_TOKEN_KEY + wxConfig.getAppID();
        String token = "";
        /*
            调用 微信公众号接口，根据 appId、appSecret 获取 access_token
         */
        final String url = String.format(WxConstant.URL_ACCESS_TOKEN_GET, wxConfig.getAppID(), wxConfig.getAppSecret());
        String result = HttpUtil.get(url);
        WxAccessTokenBean tokenBean = JsonUtils.readValue(result, WxAccessTokenBean.class);
        Long expiresIn = tokenBean.getExpiresIn();
        /*
            保存到 redis
         */
        if (StrUtil.isNotBlank(token = tokenBean.getAccessToken()) && Objects.nonNull(expiresIn)) {
            valueOperations.set(key, token, expiresIn, TimeUnit.SECONDS);
        } else {
            // access_token 异常
            log.error("errCode: {}, errMsg: {}", tokenBean.getErrCode(), tokenBean.getErrMsg());
            throw new RuntimeException("微信公众号 access_token 异常");
        }
        return token;
    }

    @Override
    public Map<String, Object> createMenu(List<WxMenuButtonBean> beans) {
        WxMenuBean menuBean = new WxMenuBean();
        menuBean.setButtons(beans);
        // 创建菜单
        String body = JsonUtils.writeValueAsString(menuBean);
        String post = HttpUtil.post(String.format(WxConstant.URL_MENU_CREATE_POST, getAccessToken()), body);
        log.info("创建微信公众号菜单\n{}\n创建信息：{}", body, post);
        return JsonUtils.readValueAsMap(post);
    }

    @Override
    public Map<String, Object> deleteMenu() {
        String result = HttpUtil.get(String.format(WxConstant.URL_MENU_DELETE_GET, getAccessToken()));
        log.info("\n删除微信公众号菜单{}", result);
        return JsonUtils.readValueAsMap(result);
    }

    @Override
    public Map<String, Object> addCustom(WxKfAccountBean bean) {
        final String url = String.format(WxConstant.URL_CUSTOM_ACCOUNT_ADD_POST, getAccessToken());
        // 密码如果没有
        if (StrUtil.isBlank(bean.getPassword())) {
            bean.setPassword("123456");
        }
        String post = HttpUtil.post(url, JsonUtils.writeValueAsString(bean));
        log.info("添加客服：{}", post);
        return JsonUtils.readValueAsMap(post);
    }

    @Override
    public Map<String, Object> delCustom(String account) {
        WxKfAccountBean accountBean = new WxKfAccountBean();
        accountBean.setKfAccount(account);
        final String url = String.format(WxConstant.URL_CUSTOM_ACCOUNT_DEL_POST, getAccessToken());
        String post = HttpUtil.post(url, JsonUtils.writeValueAsString(accountBean));
        log.info("删除客服：{}", post);
        return JsonUtils.readValueAsMap(post);
    }

    @Override
    public List<WxKfAccountBean> getKfList() {
        final String url = String.format(WxConstant.URL_CUSTOM_GET_KF_LIST_GET, getAccessToken());
        String result = HttpUtil.get(url);
        log.info("查询微信公众号所有客服：{}", result);
        return JsonUtils.readValueAsList(result, WxKfAccountBean.class);
    }

    @Override
    public WxTicketBean createTemporaryQrCode() {
        final String url = String.format(WxConstant.URL_QR_CODE_CREATE_POST, getAccessToken());
        // 生成最大失效时间 临时二维码
        String post = HttpUtil.post(url, WxQrCodeBean.getSceneStrInstance(true, UUID.randomUUID().toString(), 2592000).toJson());
        return JsonUtils.readValue(post, WxTicketBean.class);
    }

    @Override
    public void getQrCode(HttpServletResponse response) {
        /*final String url = String.format(WxConstant.URL_SHOW_QR_CODE_GET, "gQFc8DwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyM0xpZ29qRmFjVEgxMkI2WTF4Y3gAAgSleRRhAwQAjScA");

        try {
            response.reset();
            // 需要指定文件名的编码方式为 ISO-8859-1,否则会出现文件名中的中文字符丢失的问题
            response.addHeader("Content-Disposition", "attachment;filename=" + new String("测试临时二维码.png".getBytes("gb2312"), StandardCharsets.ISO_8859_1));
            // 设置响应内容类型为图片
            response.setContentType("image/png");
            long download = HttpUtil.download(url, new BufferedOutputStream(response.getOutputStream()), true);
        } catch (IOException e) {
            log.error("生成二维码失败：{}", e.getMessage());
            log.error("", e);
        }*/

        BufferedOutputStream stream = null;
        try {
            response.reset();
            // 需要指定文件名的编码方式为 ISO-8859-1,否则会出现文件名中的中文字符丢失的问题
            response.addHeader("Content-Disposition", "attachment;filename=" + new String("测试临时二维码.png".getBytes(), StandardCharsets.ISO_8859_1));
            // 设置响应内容类型为图片
            response.setContentType("image/png");
            // "http://weixin.qq.com/q/023LigojFacTH12B6Y1xcx" 是生成二维码时返回的 url
            BufferedImage image = QrCodeUtil.generate("http://weixin.qq.com/q/023LigojFacTH12B6Y1xcx", 300, 300);

            ImageIO.write(image, "png", stream = new BufferedOutputStream(response.getOutputStream()));
        } catch (IOException e) {
            log.error("生成二维码失败：{}", e.getMessage());
            log.error("", e);
        } finally {
            if (Objects.nonNull(stream)) {
                try {
                    stream.flush();
                    stream.close();
                } catch (IOException e) {
                    log.error("生成二维码关闭流失败！");
                }
            }
        }
    }

    @Override
    public Map<String, Object> setIndustry(String primaryIndustry, String secondaryIndustry) {
        final String url = String.format(WxConstant.URL_TEMPLATE_SET_INDUSTRY_POST, getAccessToken());
        String result = HttpUtil.post(url, WxTemplateSetIndustryBean.getInstance(primaryIndustry, secondaryIndustry).toJson());
        return JsonUtils.readValueAsMap(result);
    }

    @Override
    public WxTemplateIndustryBean getIndustry() {
        final String url = String.format(WxConstant.URL_TEMPLATE_GET_INDUSTRY_GET, getAccessToken());
        String result = HttpUtil.get(url);
        return JsonUtils.readValue(result, WxTemplateIndustryBean.class);
    }

    @Override
    public WxTemplateListBean getAllTemplate() {
        final String url = String.format(WxConstant.URL_TEMPLATE_GET_ALL_LIST_GET, getAccessToken());
        String result = HttpUtil.get(url);
        return JsonUtils.readValue(result, WxTemplateListBean.class);
    }

    @Override
    public WxTemplateIdBean addTemplate(String shortId) {
        final String url = String.format(WxConstant.URL_TEMPLATE_ADD_POST, getAccessToken());
        String result = HttpUtil.post(url, WxTemplateIdBean.getInstance(shortId, null).toJson());
        return JsonUtils.readValue(result, WxTemplateIdBean.class);
    }

    @Override
    public Map<String, Object> sendTemplateMsg() {
        // 模板 id
        final String templateId = "uRiti-qpU2bZSLRQk3a2zXUCZMK30MjjYQtaPkEXJYI";
        final String url = String.format(WxConstant.URL_TEMPLATE_SEND_POST, getAccessToken());
        String result = HttpUtil.post(url,
                WxTemplateMsgBean.getInstance("oqUo364fyXxJvfMM7NIwfljyeB44", templateId, "www.baidu.com")
                        .addTemplateData("first", "恭喜购买成功！", "#173177")
                        .addTemplateData("keyword1", "巧克力", "#173177")
                        .addTemplateData("keyword2", "99.99 元", "#173177")
                        .addTemplateData("keyword3", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), "#173177")
                        .addTemplateData("remark", "欢迎再次品尝！", "#173177")
                        .toJson());
        return JsonUtils.readValueAsMap(result);
    }

    /**
     * 验证 signature 是否与 token、timestamp、nonce 三个字符串"字典排序后"拼接成一个字符串进行"sha1加密"的字符串相等
     *
     * @param signature 加密后的字符串
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @param token     填写的 token
     * @return 验证成功返回 true
     */
    private boolean checkToken(String signature, String timestamp, String nonce, String token) {
        if (StrUtil.hasBlank(signature, timestamp, nonce, token)) {
            throw new IllegalArgumentException("微信公众号非法请求参数，请核实");
        }
        // 1. 将token、timestamp、nonce三个参数进行字典序排序
        String[] strArr = new String[]{timestamp, nonce, token};
        Arrays.sort(strArr);
        // 2. 将三个参数字符串拼接成一个字符串进行sha1加密
        StringBuilder builder = new StringBuilder();
        for (String s : strArr) {
            builder.append(s);
        }
        // 3. 开发者获得加密后的字符串可与signature对比，标识该请求来源于微信
//        return Objects.equals(signature, HexUtil.encodeHexStr(DigestUtil.sha1(builder.toString())));
        return Objects.equals(signature, SecureUtil.sha1(builder.toString()));
    }

    /**
     * 判断发送的消息是否是重复发送
     *
     * @param msgId        信息id
     * @param fromUserName 发送者
     * @param createTime   创建时间
     * @return 重复 true
     */
    private boolean isRepeat(Long msgId, String fromUserName, Long createTime) {
        /*
            设置到 redis 进行排重
         */
        final String key = RedisConstant.DE_WEIGHT_KEY + wxConfig.getAppID() + ":"
                + (Objects.nonNull(msgId) ? msgId : fromUserName + createTime);
        // 第一次设置会返回 true，每5秒重试一次，共重试 3 次
        // 为 null 或 false 代表 已存在，redis 不进行任何操作
        return !Objects.equals(valueOperations.setIfAbsent(key, 1, 25, TimeUnit.SECONDS), true);
    }
}
